package com.zyw.net.http;


import com.alibaba.fastjson.JSON;
import com.zyw.net.exception.HttpClientConnectException;
import com.zyw.net.exception.HttpClientReadtimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Map;

/**
 * HttpClient
 *
 * @author vill
 */
public class HttpClient {

    protected static Logger logger = LoggerFactory.getLogger(HttpClient.class);

    private static final SSLSocketFactory sslSocketFactory = initSSLSocketFactory();
    private static final TrustAnyHostnameVerifier trustAnyHostnameVerifier = new HttpClient().new TrustAnyHostnameVerifier();

    private int connectTimeout = 20000; // ms
    private int readTimeout = 20000; // ms

    /**
     * contentType 一个请求只允许设置一个 application/x-javascript text/xml->xml数据
     * application/x-javascript->json对象 application/x-www-form-urlencoded->表单数据
     */
    private String contentType = "application/x-www-form-urlencoded";
    private String charset = "UTF-8";

    public ResponseCallback defaultCallback = new ResponseCallback() {
        @Override
        public String success(String res) {
            return res;
        }

        @Override
        public String error(String errMsg) {
            return errMsg;
        }
    };

    private HttpClient() {
    }

    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public void setReadTimeout(int readTimeout) {
        this.readTimeout = readTimeout;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public void setCharset(String charset) {
        this.charset = charset;
    }

    public static HttpClient newHttpClient() {
        return new HttpClient();
    }

    public HttpClient connectTimeout(int connectTimeout) {
        this.setConnectTimeout(connectTimeout);
        return this;
    }

    public HttpClient readTimeout(int readTimeout) {
        this.setReadTimeout(readTimeout);
        return this;
    }

    public HttpClient contentType(String contentType) {
        this.setContentType(contentType);
        return this;
    }

    public HttpClient charset(String charset) {
        this.setCharset(charset);
        return this;
    }

    /**
     * 获取连接
     *
     * @param url     请求地址
     * @param method  请求方式
     * @param headers 请求头
     * @return connection
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws NoSuchProviderException
     * @throws KeyManagementException
     */
    private HttpURLConnection getHttpConnection(String url, String method, Map<String, String> headers,
                                                Map<String, Object> config)
            throws IOException, NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException {
        URL _url = new URL(url);
        HttpURLConnection conn = (HttpURLConnection) _url.openConnection();
        if (conn instanceof HttpsURLConnection) {
            ((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory);
            ((HttpsURLConnection) conn).setHostnameVerifier(trustAnyHostnameVerifier);
        }
        // 设置超时时间
        if (config == null) {
            conn.setConnectTimeout(this.connectTimeout);
            conn.setReadTimeout(this.readTimeout);
        } else {
            Object _connectTimeout = config.get("connectTimeout");
            Object _readTimeout = config.get("readTimeout");
            conn.setConnectTimeout(_connectTimeout == null ? this.connectTimeout : (Integer) _connectTimeout);
            conn.setReadTimeout(_readTimeout == null ? this.readTimeout : (Integer) _readTimeout);
        }

        conn.setRequestMethod(method);
        conn.setDoOutput(true);
        conn.setDoInput(true);

        if (RequestMethod.POST.name().equals(method)) {// 只有post请求需要设置contenttype
            conn.setRequestProperty("Content-Type", this.contentType);
            conn.setRequestProperty("User-Agent", "huoli_agent");
        }

        if (headers != null && !headers.isEmpty())
            for (Map.Entry<String, String> entry : headers.entrySet())
                conn.setRequestProperty(entry.getKey(), entry.getValue());

        return conn;
    }

    public String get(String url) {
        return get(url, null, null);
    }

    public String get(String url, ResponseCallback callback) {
        return get(url, null, callback);
    }

    public String get(String url, HttpParams params) {
        return get(url, params, null);
    }

    /**
     * Send GET request
     */
    public String get(String url, HttpParams params, ResponseCallback callback) {
        if (callback == null) {
            callback = defaultCallback;
        }
        return doRequest(url, RequestMethod.GET, params, callback);
    }

    public String post(String url, HttpParams params) {
        return post(url, params, null);
    }

    /**
     * Send POST request 现只支持表单提交
     */
    public String post(String url, HttpParams params, ResponseCallback callback) {
        if (callback == null) {
            callback = defaultCallback;
        }
        return doRequest(url, RequestMethod.POST, params, callback);
    }

    /**
     * 发送请求默认方法
     *
     * @param url        请求地址
     * @param method     请求方式
     * @param httpParams 请求参数
     */
    private String doRequest(final String url, final RequestMethod method, final HttpParams httpParams,
                             final ResponseCallback callback) {
        HttpURLConnection conn = null;
        long startTime = System.currentTimeMillis();

        // 请求参数
        Map<String, String> params = null;
        // 请求头
        Map<String, String> headers = null;
        // 请求配置
        Map<String, Object> config = null;

        if (httpParams != null) {
            params = httpParams.getRequestData();
            headers = httpParams.getRequestHeader();
            config = httpParams.getRequestConf();
        }

        logger.debug("HTTP {} {} params {}", method, url, params);
        try {
            switch (method) {
                case GET:
                    conn = getHttpConnection(buildUrlWithQueryString(url, params), RequestMethod.GET.name(), headers,
                            config);
                    conn.connect();
                    break;
                case POST:
                    conn = getHttpConnection(url, RequestMethod.POST.name(), headers, config);
                    conn.connect();
                    String data = null;
                    if (params != null) {
                        data = encodeParams(params);
                    } else if (httpParams.getHttpBody() != null) {
                        data = JSON.toJSONString(httpParams.getHttpBody());
                    }
                    OutputStream out = conn.getOutputStream();
                    out.write(data.getBytes(this.charset));// 提交post请求参数
                    out.flush();
                    out.close();
                    break;
                case PUT:
                    logger.warn("PUT is not support now ");
                    throw new UnsupportedOperationException("PUT is not support now ");
                case DELETE:
                    logger.warn("DELETE is not support now ");
                    throw new UnsupportedOperationException("DELETE is not support now ");
            }
        } catch (Exception e) {
            // 连接出异常了，尝试关闭connection
            if (conn != null) {
                conn.disconnect();
            }
            throw new HttpClientConnectException("request url " + url + " has broke: " + e.getMessage(), e.getCause());
        }
        logger.debug("HTTP connect cost:{}ms", System.currentTimeMillis() - startTime);

        try {
            String res;
            long readStart = System.currentTimeMillis();
            if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                res = readResponseString(conn);
                logger.debug("HTTP read cost:{}ms", System.currentTimeMillis() - readStart);
                logger.debug("HTTP response status {}/OK", conn.getResponseCode());
                logger.debug("HTTP response data {}", res != null ? res.replaceAll("\n", "") : "");
                return callback.success(res);
            } else {
                res = readErrorResponseString(conn);
                logger.debug("HTTP read cost:{}ms", System.currentTimeMillis() - readStart);
                logger.debug("HTTP {} {}:reason：{}", method, conn.getResponseCode(), res);
                callback.error(res);
            }
            return null;
        } catch (IOException e) {
            throw new HttpClientReadtimeoutException("request url " + url + " has broke: " + e.getMessage(),
                    e.getCause());
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    /**
     * 读取响应参数
     *
     * @param conn 连接
     * @return string
     */
    private String readResponseString(HttpURLConnection conn) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        try {
            inputStream = conn.getInputStream();
            if (inputStream != null) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, this.charset));
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line).append("\n");
                }
            }
            return sb.toString();
        } catch (IOException e) {
            throw new HttpClientReadtimeoutException(e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 读取失败的响应流
     *
     * @param conn 连接
     * @return
     */
    private String readErrorResponseString(HttpURLConnection conn) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        try {
            inputStream = conn.getErrorStream();
            if (inputStream != null) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, this.charset));
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line).append("\n");
                }
            }
            return sb.toString();
        } catch (IOException e) {
            throw new HttpClientReadtimeoutException(e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Build queryString of the url 主要用于 get请求
     */
    private String buildUrlWithQueryString(String url, Map<String, String> params) {
        if (params == null || params.isEmpty())
            return url;

        StringBuilder sb = new StringBuilder(url);
        boolean isFirst;
        if (url.indexOf("?") == -1) {
            isFirst = true;
            sb.append("?");
        } else {
            isFirst = false;
        }

        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (isFirst)
                isFirst = false;
            else
                sb.append("&");

            String key = entry.getKey();
            String value = entry.getValue();
            if (value != null && !"".equals(value))
                try {
                    value = URLEncoder.encode(value, this.charset);
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException(e);
                }
            sb.append(key).append("=").append(value);
        }
        return sb.toString();
    }

    /**
     * 将map格式的请求参数，按照指定的编码格式转换成form表单提交需要的格式及 a=b&c=d 主要用于post请求
     *
     * @param params map格式的请求参数
     * @return string
     */
    public String encodeParams(Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        boolean isFirst = true;
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (isFirst) {
                isFirst = false;
            } else {
                sb.append("&");
            }

            String key = entry.getKey();
            String value = entry.getValue();
            if (value != null && !"".equals(value))
                try {
                    value = URLEncoder.encode(value, this.charset);
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException(e);
                }
            sb.append(key).append("=").append(value);
        }
        return sb.toString();
    }

    enum RequestMethod {
        GET, POST, PUT, DELETE
    }

    /**
     * 回调协议
     */
    public interface ResponseCallback {

        /**
         * 成功回调
         */
        String success(String res);

        /**
         * 失败回调
         */
        String error(String errMsg);

    }

    /**
     * https 域名校验
     */
    private class TrustAnyHostnameVerifier implements HostnameVerifier {
        public boolean verify(String hostname, SSLSession session) {
            return true;
        }
    }

    /**
     * https 证书管理
     */
    private class TrustAnyTrustManager implements X509TrustManager {
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }

        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
    }

    private static SSLSocketFactory initSSLSocketFactory() {
        try {
            TrustManager[] tm = {new HttpClient().new TrustAnyTrustManager()};
            SSLContext sslContext = SSLContext.getInstance("TLS", "SunJSSE");
            sslContext.init(null, tm, new java.security.SecureRandom());
            return sslContext.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
